# -*- coding: utf-8 -*-
#
# Copyright 2009 Vanderbilt University
# 
# Licensed under the Apache License, Version 2.0 (the "License"); 
# you may not use this file except in compliance with the License. 
# You may obtain a copy of the License at 
# 
#     http://www.apache.org/licenses/LICENSE-2.0 
# 
# Unless required by applicable law or agreed to in writing, software 
# distributed under the License is distributed on an "AS IS" BASIS, 
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
# See the License for the specific language governing permissions and 
# limitations under the License. 

"""Utility methods for HORNET.  Includes functions to deal with time 
(:func:`hornet.util.make_periods`) and decorators 
(:func:`hornet.util.memoize`)

.. moduleauthor:: John Paulett <john.paulett -at- vanderbilt.edu>
"""
from collections import defaultdict
from functools import wraps
import operator

class Period(tuple):
    """Simple data structure representing a period in time with a start and end.
    This class provides formatting for the period.
    
    >>> p = Period(datetime.date(2008, 1, 1), datetime.date(2008, 1, 8))
    >>> repr(p)
    '<Period(datetime.date(2008, 1, 1), datetime.date(2008, 1, 8))>'
    >>> str(p)
    '20080101-20080108'
    """
    def __new__(klass, start, end):
        return super(Period, klass).__new__(klass, (start, end))
    
    def __str__(self):
        return '-'.join([d.strftime('%Y%m%d') for d in self]) #TODO handle times
    
    def __repr__(self):
        return '<%s(%s, %s)>' % (str(self.__class__.__name__), repr(self[0]), repr(self[1]))
    
    def __getnewargs__(self):
        ## work around for pickling subclass of a tuple with custom __new__. 
        ## From Alex Martelli:
        ## http://coding.derkeiler.com/Archive/Python/comp.lang.python/2005-01/0082.html
        return self[0], self[1] 
        
def make_periods(start, end, period_size):
    """With a *start* and an *end* date, splits the time range up into 
    sub-periods of size *period_size*. *start* and *end* must be 
    :class:`~datetime.date` objects and *period_size* must be
    a :class:`~datetime.timedelta` object
    
    >>> import datetime
    >>> make_periods(datetime.date(2008, 1, 1), datetime.date(2008, 1, 9), datetime.timedelta(7))
    [<Period(datetime.date(2008, 1, 1), datetime.date(2008, 1, 8))>, <Period(datetime.date(2008, 1, 8), datetime.date(2008, 1, 9))>]
    """
    # TODO consider implications of database is 1/1/06 less than or equal t
    # 1/1/6 14:49?  implications on size and lessthan/greaterthan
    periods = []
    early = start
    future = start + period_size
    while future < end:
        periods.append(Period(early, future))
        early += period_size
        future += period_size
    if early < end:
        periods.append(Period(early, end))
        
    return periods

def memoize(func):    
    """Decorator that caches a function's return value each time it is called.
    If called later with the same arguments, the cached value is returned, and
    not re-evaluated.  From the `Python Decorator Library 
    <http://wiki.python.org/moin/PythonDecoratorLibrary>`_ and the `decorator
    module <http://pypi.python.org/pypi/decorator>`_
    
    >>> i = iter([1, 10, 100])
    >>> f = memoize(lambda x: x * i.next())
    >>> f(4)
    4
    >>> f(7)
    70
    >>> f(4) # notice how 4 * 1 is returned and not 4 * 100
    4
    """
    func.cache = {}
    @wraps(func)
    def new(*args, **kw):
        if kw: # frozenset is used to ensure hashability
            key = args, frozenset(kw.iteritems())
        else:
            key = args
        try:
            if key in func.cache:
                return func.cache[key]
            else:
                func.cache[args] = value = func(*args, **kw)
                return value
        except TypeError:
            # always compute on parameters that are unhashable like lists
            return func(*args, **kw)
    return new

def count_unique(list):
    """Finds the count of each value in *list*. Returns a dictionary with the
    key as a unique value, and the value as the number of occurrences of that
    value in the *values*.
    
    >>> count_unique([1, 2, 3, 1, 1, 2])
    {1: 3, 2: 2, 3: 1}
    """
    retval = defaultdict(int)
    for v in list:
        retval[v] += 1
    return dict(retval)

def accumulate(data):
    """Computes the integral of *data*. *data* is a dictionary. Returns a 
    dictionary with the same keys as in *data*. It is assumed that all values
    greater or equal than a given key before the next highest key value are 
    the same as the value at the given key. 
    
    >>> data = {0.25: 1, 0.5: 2, 0.75: 4, 1: 8}
    >>> accumulate(data)
    {0.25: 1, 0.5: 3, 0.75: 7, 1: 15}
    """ 
    cdf = {}
    cumulative = 0
    list = sorted(data.items(), key=operator.itemgetter(0))
    for k, v in list:
        cumulative += v
        cdf[k] = cumulative
    return cdf

def dict_apply(data, value_func):
    """Applies the function *value_func* to every value in the dictionary, 
    *data*. Returns a new copy of the dict, leaving the original unmodified.
    
    >>> dict_apply({'a': 1, 'b': 'c'}, lambda x: x * 3)
    {'a': 3, 'b': 'ccc'}
    """
    temp = ((k, value_func(v)) for k, v in data.iteritems())
    return dict(temp)

def extract_attr(iterable, attr):
    """Takes an iterable, such as a list, and pulls out the attribute of each
    object, specified by *attr*. Returns a list.
    
    >>> extract_attr([object(), list(), dict()], '__class__')
    [<type 'object'>, <type 'list'>, <type 'dict'>]
    """
    return [getattr(v, attr) for v in iterable]

